use crate::FileNode;
use snafu::ResultExt;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use uuid::Uuid;

pub fn build_tree(input_dir: &Path, max_file_size: Option<u64>) -> crate::Result<FileNode> {
    let mut root = build_tree_r(input_dir, input_dir)?;
    prune_tree(&mut root, max_file_size);
    Ok(root)
}

fn build_tree_r(base: &Path, dir: &Path) -> crate::Result<FileNode> {
    let mut children = vec![];

    let mut entries: Vec<fs::DirEntry> = fs::read_dir(dir)
        .whatever_context("Fail to read dir")?
        .filter_map(Result::ok)
        .collect();

    entries.sort_by_key(|e| e.file_name());

    for entry in entries {
        let full_path = entry.path();
        let file_type = entry
            .file_type()
            .whatever_context("Fail to get file type")?;

        if file_type.is_dir() {
            let child = build_tree_r(base, &full_path)?;

            children.push(child);
        } else if file_type.is_file() {
            let rel_path = full_path
                .strip_prefix(base)
                .whatever_context("Fail to strip prefix")?
                .to_path_buf();

            let metadata = entry.metadata().whatever_context("Fail to get metadata")?;
            let file_size = metadata.len();

            let file_uuid = Uuid::new_v4();

            let is_binary = is_probably_binary(&full_path)?;

            children.push(FileNode::File(rel_path, file_uuid, is_binary, file_size));
        }
    }

    Ok(FileNode::Dir(children))
}

fn is_probably_binary(path: &Path) -> crate::Result<bool> {
    const SAMPLE_SIZE: usize = 4096;
    const RATIO: f32 = 0.3;

    let mut file = fs::File::open(path).whatever_context("Could not open file")?;
    let mut buf = [0u8; SAMPLE_SIZE];

    let n = file
        .read(&mut buf)
        .whatever_context("Fail to read file for binary check")?;

    let slice = &buf[..n];

    if slice.is_empty() {
        return Ok(false);
    }

    if slice.contains(&0) {
        return Ok(true);
    }

    let mut control_count = 0usize;

    for &b in slice {
        if b == b'\n' || b == b'\r' || b == b'\t' {
            continue;
        }

        if b < 0x20 || b == 0x7F {
            control_count += 1;
        }
    }

    let ratio = control_count as f32 / slice.len() as f32;

    Ok(ratio > RATIO)
}

pub fn prune_tree(root: &mut FileNode, max_file_size: Option<u64>) {
    prune_r(root, max_file_size);
}

fn prune_r(node: &mut FileNode, max_file_size: Option<u64>) -> bool {
    match node {
        FileNode::Dir(children) => {
            let mut i = 0;
            while i < children.len() {
                if prune_r(&mut children[i], max_file_size) {
                    i += 1
                } else {
                    children.remove(i);
                }
            }

            !children.is_empty()
        }
        FileNode::File(_path, _uuid, is_binary, size) => {
            if *is_binary {
                return false;
            }
            if let Some(max_file_size) = max_file_size {
                let sz = *size;
                if sz > max_file_size {
                    return false;
                }
            }
            true
        }
    }
}

/*
$$$$INDEX-START$$$

/src/main.rs->[uuid=19a2b6b0-0e25-4d52-b7f8-1fd5058cd222]
/src/lib.rs->[uuid=9e7c1f1e-3e11-4e3c-b673-9c7dfb28a333]

$$$$INDEX-END$$$
*/
pub fn print_index(root: &FileNode) -> super::Result<()> {
    let files = travel_tree(root);

    println!("$$$$INDEX-START$$$$");
    println!();

    for (path, uuid) in files {
        let path_str = normalize_path(&path);
        println!("{path_str}->[uuid={uuid}]");
    }

    println!();
    println!("$$$$INDEX-END$$$$");

    Ok(())
}

fn travel_tree(root: &FileNode) -> Vec<(PathBuf, Uuid)> {
    let mut files = vec![];
    collect_files(root, &mut files);
    files.sort_by(|(p1, _), (p2, _)| p1.cmp(p2));
    files
}

fn collect_files(node: &FileNode, out: &mut Vec<(PathBuf, Uuid)>) {
    match node {
        FileNode::Dir(children) => {
            for child in children {
                collect_files(child, out);
            }
        }
        FileNode::File(path, uuid, _is_binary, _size) => {
            out.push((path.clone(), *uuid));
        }
    }
}

fn normalize_path(path: &Path) -> String {
    let s = path.to_string_lossy().replace('\\', "/");
    if s.starts_with('/') {
        s.to_owned()
    } else {
        format!("/{}", s)
    }
}

pub fn print_context(
    root: &FileNode,
    include_exits: Vec<String>,
    exclude_exits: Vec<String>,
    base: &Path,
) -> crate::Result<()> {
    let files = travel_tree(root);

    println!("$$$CONTEXT-START$$$");
    println!();

    for (rel_path, uuid) in files {
        if !remain_file(&rel_path, &include_exits, &exclude_exits) {
            continue;
        }

        let path_str = normalize_path(&rel_path);
        println!("@@@@FILE START@@@@-PATH={path_str}&&UUID={uuid}");

        let full_path = base.join(&rel_path);
        let content =
            fs::read_to_string(&full_path).whatever_context("Fail to read file content")?;

        print!("{}", content);
        if !content.ends_with('\n') {
            println!();
        }

        println!("@@@@FILE END@@@@-PATH={path_str}&&UUID={uuid}");
        println!();
    }
    println!("$$$CONTEXT-END$$$");

    Ok(())
}

fn remain_file(path: &Path, include_exits: &[String], exclude_exits: &[String]) -> bool {
    let ext = path
        .extension()
        .and_then(|os| os.to_str())
        .map(|s| s.trim_start_matches('.').to_ascii_lowercase())
        .unwrap_or_default();

    if !include_exits.is_empty() {
        return include_exits.contains(&ext);
    }

    if !exclude_exits.is_empty() {
        return !exclude_exits.contains(&ext);
    }

    true
}
